package com.strato.saas.merchant.v1_0_0.datasource;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.logging.Logger;

import javax.sql.DataSource;

import com.alibaba.druid.filter.Filter;
import com.alibaba.druid.pool.DruidDataSource;
import com.strato.base.cache.v1_0_0.TimedReference;
import com.strato.base.env.v1_0_0.GlobalEnvironment;
import com.strato.jdbc.datasource.v1_0_0.DynamicDataSource;
import com.strato.saas.merchant.v1_0_0.MerchantContextHolder;
import com.strato.saas.merchant.v1_0_0.MerchantStore;

public class MerchantAwareDataSource implements DynamicDataSource {

	private MerchantStore merchantStore;
	
	private Map<String,TimedReference<DruidDataSource>> dataSources=new HashMap<String,TimedReference<DruidDataSource>>();
	
	public void setMerchantStore(MerchantStore merchantStore) {
		this.merchantStore = merchantStore;
	}

	private long maxWait=60000;
	
	private long timeBetweenEvictionRunsMillis=60000;
	
	private long minEvictableIdleTimeMillis=300000;
	
	private String validationQuery="select 'x'";
	
	private boolean testWhileIdle=true;
	
	private boolean testOnBorrow=true;
	
	private boolean testOnReturn=true;
	
	private boolean poolPreparedStatements=false;
	
	private int maxPoolPreparedStatementPerConnectionSize=20;
	
	private boolean defaultAutoCommit=true;
	
	private boolean defaultReadOnly=false;
	
	private List<Filter> filters;

	private boolean removeAbandoned=true;

	private int removeAbandonedTimeout=2000;

	private boolean logAbandoned=true;
	
	private Integer initialSize=0;
	
	private Integer maxActive=5;
	
	private int cacheTTL=5*60*1000;
	
	public Integer getInitialSize() {
		return initialSize;
	}

	public void setInitialSize(Integer initialSize) {
		this.initialSize = initialSize;
	}

	public Integer getMaxActive() {
		return maxActive;
	}

	public void setMaxActive(Integer maxActive) {
		this.maxActive = maxActive;
	}

	public void init(){
		Timer timer=new Timer();
		TimerTask task=new TimerTask() {
			@Override
			public void run() {
				MerchantAwareDataSource.this.collectUnused();
			}
		};
		timer.schedule(task, cacheTTL, cacheTTL);
	}
	
	protected void collectUnused() {
		Map<String,TimedReference<DruidDataSource>> dss=new HashMap<String,TimedReference<DruidDataSource>>();
		dss.putAll(dataSources);
		for(Iterator<Entry<String,TimedReference<DruidDataSource>>> iter=dss.entrySet().iterator();iter.hasNext();){
			Map.Entry<String,TimedReference<DruidDataSource>> entry=iter.next();
			String key=entry.getKey();
			TimedReference<DruidDataSource> ref = entry.getValue();
			if(ref!=null&&!ref.isExpired()){
				DruidDataSource ds = ref.getObject();
				if(ds==null){
					continue;
				}
				if(ds.getActiveCount()!=0){
					continue;
				}
				ds.close();
				dataSources.remove(key);
			}
		}
	}

	public int getCacheTTL() {
		return cacheTTL;
	}

	public void setCacheTTL(int cacheTTL) {
		this.cacheTTL = cacheTTL;
	}

	public boolean isLogAbandoned() {
		return logAbandoned;
	}

	public void setLogAbandoned(boolean logAbandoned) {
		this.logAbandoned = logAbandoned;
	}

	public boolean isRemoveAbandoned() {
		return removeAbandoned;
	}

	public void setRemoveAbandoned(boolean removeAbandoned) {
		this.removeAbandoned = removeAbandoned;
	}

	public int getRemoveAbandonedTimeout() {
		return removeAbandonedTimeout;
	}

	public void setRemoveAbandonedTimeout(int removeAbandonedTimeout) {
		this.removeAbandonedTimeout = removeAbandonedTimeout;
	}

	public long getMaxWait() {
		return maxWait;
	}

	public void setMaxWait(long maxWait) {
		this.maxWait = maxWait;
	}

	public long getTimeBetweenEvictionRunsMillis() {
		return timeBetweenEvictionRunsMillis;
	}

	public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) {
		this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
	}

	public long getMinEvictableIdleTimeMillis() {
		return minEvictableIdleTimeMillis;
	}

	public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) {
		this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
	}

	public String getValidationQuery() {
		return validationQuery;
	}

	public void setValidationQuery(String validationQuery) {
		this.validationQuery = validationQuery;
	}

	public boolean isTestWhileIdle() {
		return testWhileIdle;
	}

	public void setTestWhileIdle(boolean testWhileIdle) {
		this.testWhileIdle = testWhileIdle;
	}

	public boolean isTestOnBorrow() {
		return testOnBorrow;
	}

	public void setTestOnBorrow(boolean testOnBorrow) {
		this.testOnBorrow = testOnBorrow;
	}

	public boolean isTestOnReturn() {
		return testOnReturn;
	}

	public void setTestOnReturn(boolean testOnReturn) {
		this.testOnReturn = testOnReturn;
	}

	public boolean isPoolPreparedStatements() {
		return poolPreparedStatements;
	}

	public void setPoolPreparedStatements(boolean poolPreparedStatements) {
		this.poolPreparedStatements = poolPreparedStatements;
	}

	public int getMaxPoolPreparedStatementPerConnectionSize() {
		return maxPoolPreparedStatementPerConnectionSize;
	}

	public void setMaxPoolPreparedStatementPerConnectionSize(
			int maxPoolPreparedStatementPerConnectionSize) {
		this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
	}

	public boolean isDefaultAutoCommit() {
		return defaultAutoCommit;
	}

	public void setDefaultAutoCommit(boolean defaultAutoCommit) {
		this.defaultAutoCommit = defaultAutoCommit;
	}

	public boolean isDefaultReadOnly() {
		return defaultReadOnly;
	}

	public void setDefaultReadOnly(boolean defaultReadOnly) {
		this.defaultReadOnly = defaultReadOnly;
	}

	public List<Filter> getFilters() {
		return filters;
	}

	public void setFilters(List<Filter> filters) {
		this.filters = filters;
	}

	private synchronized DataSource obtainDataSource(String merchantId){
		{
			TimedReference<DruidDataSource> ref=dataSources.get(merchantId);
			if(ref!=null){
				DruidDataSource ds=ref.getObject();
				if(ds!=null){
					if(ref.isExpired()){
						ds.close();
						dataSources.remove(merchantId);
					}else{
						ref.setCacheTime(System.currentTimeMillis());
						return ds;
					}
				}
			}
		}
		@SuppressWarnings("unchecked")
		Map<String,Object> merchant=(Map<String,Object>) merchantStore.load(merchantId);
		if(merchant==null){
			throw new IllegalArgumentException("Merchant "+merchantId+" is no longer valid");
		}
		String mode=GlobalEnvironment.getInstance().get(GlobalEnvironment.KEY_MODE,GlobalEnvironment.VALUE_MODE_DEVELOPMENT);
		boolean production=GlobalEnvironment.VALUE_MODE_PRODUCTION.equals(mode);
		String url=(String) merchant.get("conn_url");
		if(production){
			url=resolveProductionUrl(url);
		}
		String username=(String) merchant.get("conn_usr");
		String password=(String) merchant.get("conn_pwd");
		int minsize=(Integer) merchant.get("conn_minsize");
		DruidDataSource dataSource=new DruidDataSource();
		dataSource.setDriverClassName(GlobalEnvironment.getInstance().get("merchantDS.driver","com.mysql.jdbc.Driver"));
		dataSource.setUrl(url);
		dataSource.setUsername(username);
		dataSource.setPassword(password);
		if(this.initialSize!=null){
			dataSource.setInitialSize(this.initialSize);
		}else{
			int initsize=(Integer) merchant.get("conn_initsize");
			dataSource.setInitialSize(initsize);
		}
		if(this.maxActive!=null){
			dataSource.setMaxActive(this.maxActive);
		}else{
			int maxsize=(Integer) merchant.get("conn_maxsize");
			dataSource.setMaxActive(maxsize);
		}
		
		dataSource.setMinIdle(minsize);
		dataSource.setMaxWait(maxWait);
		dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
		dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
		dataSource.setValidationQuery(validationQuery);
		dataSource.setTestWhileIdle(testWhileIdle);
		dataSource.setTestOnBorrow(testOnBorrow);
		dataSource.setTestOnReturn(testOnReturn);
		dataSource.setPoolPreparedStatements(poolPreparedStatements);
		dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
		dataSource.setRemoveAbandoned(removeAbandoned);
		dataSource.setRemoveAbandonedTimeout(removeAbandonedTimeout);
		dataSource.setProxyFilters(filters);
		dataSource.setDefaultAutoCommit(defaultAutoCommit);
		dataSource.setDefaultReadOnly(defaultReadOnly);
		dataSource.setLogAbandoned(logAbandoned);
		try {
			dataSource.init();
		} catch (SQLException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
		TimedReference<DruidDataSource> ref=new TimedReference<DruidDataSource>(dataSource,cacheTTL);
		dataSources.put(merchantId, ref);
		return dataSource;
	}
	
	private static String RESOLVER_PREFIX="urlresolver.";
	
	private String resolveProductionUrl(String url) {
		Map<String, String> env = GlobalEnvironment.getInstance().getEnv();
		for(Iterator<Entry<String, String>> iter = env.entrySet().iterator();iter.hasNext();){
			Entry<String, String> entry = iter.next();
			String key=entry.getKey();
			if(key.startsWith(RESOLVER_PREFIX)){
				String devUrl=key.substring(RESOLVER_PREFIX.length());
				String prdUrl=entry.getValue();
				url=url.replaceAll(devUrl, prdUrl);
			}
		}
		return url;
	}

	@Override
	public Connection getConnection() throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		throw new UnsupportedOperationException();
	}
	
	public void close(){
		throw new UnsupportedOperationException();
	}

	@Override
	public PrintWriter getLogWriter() throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public int getLoginTimeout() throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public Logger getParentLogger() throws SQLFeatureNotSupportedException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setLogWriter(PrintWriter arg0) throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setLoginTimeout(int arg0) throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean isWrapperFor(Class<?> arg0) throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public <T> T unwrap(Class<T> arg0) throws SQLException {
		throw new UnsupportedOperationException();
	}

	@Override
	public DataSource getDyamicDataSource() {
		String merchantId=MerchantContextHolder.get();
		DataSource ds=obtainDataSource(merchantId);
		return ds;
	}

}
